package com.weatherinczbackend;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamResult;
import net.sf.saxon.Configuration;
import net.sf.saxon.query.DynamicQueryContext;
import net.sf.saxon.query.StaticQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.trans.XPathException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.weatherinczbackend.exceptions.InternalErrorException;
import com.weatherinczbackend.exceptions.NullObjectException;
import java.io.FileReader;
import java.util.Properties;

/**
 * FileWithWeatherParser:
 * 
 * Function:
 * - parse file htmlFile to XML xmlFile
 * - use xquery file to convert data to xml
 * 
 * Expecting existence of:
 * - htmlFile : file with prepared table of weather in some year
 * - weather.xq : file with xquery code to get XML from htmlFile
 *
 * Deprycated code: (still can be used but its not recommended
 * - old parser which not use xquery but parse code line by line
 * - map temperatureInAreas where was save data from parsing of htmlFile
 * - Patterns and prefixes used for parsing by old parser
 * 
 * @author Bc. Michal Stava 255739 <255739@mail.muni.cz>
 */
public class FileWithWeatherParser {
    
    //xquery_file_name - file name of xquery file used to converting htmlFile to xmlFile
    private static final String XQUERY_FILE_NAME = "weather.xq";
    
    //htmlFile - File where table with weather must be prepared
    private File htmlFile;
    //xmlFile - File to where will be xml generated
    private File xmlFile;
    //outputCharset - charset of xmlFile (create xmlFile with charset outputCharset)
    private String outputCharset;
    //year - year of new xmlFile (use for name year.xml - ex. 1999.xml)
    private String year;
    
    //temperatureInAreas - data structure to save parsed data from htmlFile
    @Deprecated
    private Map<String, String[][]> temperatureInAreas;
    @Deprecated
    private static final Pattern temperature = Pattern.compile("([>]-?\\d+[,]\\d+[<])|(number[\"][>][<])");
    @Deprecated
    private static final Pattern areas = Pattern.compile("([3]|[\"])[>][a-z ěšňčřžýáíéúů]+[<]");
    @Deprecated
    private static final String prefixForOldParser = new String("<tr class=\"portlet-table-body\">");
    
    /**
     * Constructor 1
     * - outputCharset automatic set on UTF-8 (can be set manual later)
     * - htmlFile is set by constructor (need to set way to file with weather table)
     * - year is only needed for name of outputXml file
     * 
     * Deprycated
     * - temperatureInAreas is initialized
     * 
     * @param htmlFile file with weather table
     * @param year year of weather table
     */
    public FileWithWeatherParser(File htmlFile, String year) {
        this.htmlFile = htmlFile;
        this.outputCharset = "UTF-8";
        this.temperatureInAreas = new HashMap<String, String[][]>();
        this.year = year;
    }
    
    /**
     * Constructor 2
     * - the same like Constructor 1 + manual set of outputCharset
     * 
     * @param htmlFile file with weather table
     * @param year year of weather table
     * @param outputCharset outputCharset (UTF-8, 8859_2)
     */
    public FileWithWeatherParser(File htmlFile, String year, String outputCharset) {
        this(htmlFile, year);
        this.outputCharset = outputCharset;
    }

    /**
     * Get method for year
     * @return year
     */
    public String getYear() {
        return year;
    }

    /**
     * Set method for year
     * @param year 
     */
    public void setYear(String year) {
        this.year = year;
    }
    
    /**
     * Get method for htmlFile
     * @return htmlFile
     */
    public File getHtmlFile() {
        return htmlFile;
    }

    /**
     * Set method for htmlFile
     * @param htmlFile 
     */
    public void setHtmlFile(File htmlFile) {
        this.htmlFile = htmlFile;
    }
    
    /**
     * Get method for xmlFile
     * @return xmlFile
     */
    public File getXmlFile() {
        return xmlFile;
    }

    /**
     * Set method for xmlFile
     * @param xmlFile
     */
    public void setXmlFile(File xmlFile) {
        this.xmlFile = xmlFile;
    }

    /**
     * Get method for outputCharset
     * @return outputCharset
     */
    public String getOutputCharset() {
        return outputCharset;
    }

    /**
     * Set method for outputCharset
     * @param outputCharset
     */
    public void setOutputCharset(String outputCharset) {
        this.outputCharset = outputCharset;
    }

    /**
     * Get method for temperatureInAreas
     * @return temperatureInAreas map
     */
    public Map<String, String[][]> getTemperatureInAreas() {
        return temperatureInAreas;
    }

    /**
     * Set method for temperatureInAreas
     * @param temperatureInAreas 
     */
    public void setTemperatureInAreas(Map<String, String[][]> temperatureInAreas) {
        this.temperatureInAreas = temperatureInAreas;
    }
    
    /**
     * Set outputCharset on UTF-8
     */
    public void setOutputCharsetToUTF8() {
        this.outputCharset = "UTF-8";
    }
    
    /**
     * Set outputCharset on ISO 8859_2
     */
    public void setOutputCharsetToISO_8859_2() {
        this.outputCharset = "8859_2";
    }

    /**
     * Method for hashCode.
     * @return hash int
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 97 * hash + Objects.hashCode(this.htmlFile);
        hash = 97 * hash + Objects.hashCode(this.xmlFile);
        hash = 97 * hash + Objects.hashCode(this.outputCharset);
        hash = 97 * hash + Objects.hashCode(this.year);
        hash = 97 * hash + Objects.hashCode(this.temperatureInAreas);
        return hash;
    }

    /**
     * Equals method this object with obj.
     * @param obj
     * @return true if object are equivalent, false if not
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final FileWithWeatherParser other = (FileWithWeatherParser) obj;
        if (!Objects.equals(this.htmlFile, other.htmlFile)) {
            return false;
        }
        if (!Objects.equals(this.xmlFile, other.xmlFile)) {
            return false;
        }
        if (!Objects.equals(this.outputCharset, other.outputCharset)) {
            return false;
        }
        if (!Objects.equals(this.year, other.year)) {
            return false;
        }
        if (!Objects.equals(this.temperatureInAreas, other.temperatureInAreas)) {
            return false;
        }
        return true;
    }

    /**
     * This obj to string.
     * @return this obj in string
     */
    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "{" + "htmlFile=" + htmlFile + ", xmlFile=" + xmlFile + ", outputCharset=" + outputCharset + ", year=" + year + ", temperatureInAreas=" + temperatureInAreas + '}';
    }
    
    /**
     * Parse file with weather table by xquery parser.
     * - more info in parseWeatherFile(boolean useOldParser)
     * 
     * @throws InternalErrorException when some internal error occurs
     * @throws IOException input output exception when works with files
     * @throws XPathException when xquery parser exception occurs
     */
    public void parseWeatherFile() throws InternalErrorException, IOException, XPathException {
        this.parseWeatherFile(false);
    }
    
    /**
     * NewParser: (xquery)
     * - Parse htmlFile. 
     * - If some error occurs throw InternalError or XPath exception.
     * - If everything is ok and parsing is done, save xml to ?.xml file
     * -- ? = year (ex. 1999)
     * -- ?.xml file will be saved in current working directory in directory 'xmls' (ex. xmls/1999.xml)
     * 
     * OldParser: (text parser)
     * - Parse htmlFile.
     * - If some error occurs throw InternalError exception.
     * - If everything is ok, save data to map temperatureInAreas (need to save to xml file after that)
     * -- for file save, need to use method of this object convertDataToXml
     *
     * Needed:
     * - outputCharset set
     * - htmlFile set and exists
     * - year set
     * 
     * @throws InternalErrorException when some internal error occurs
     * @throws IOException input output exception when works with files
     * @throws XPathException when xquery parser exception occurs
     */
    public void parseWeatherFile(boolean useOldParser) throws InternalErrorException, IOException, XPathException {
        if(this.outputCharset == null) throw new NullObjectException("OutputCharset can't be null", "outputCharset");
        if(this.htmlFile == null) throw new NullObjectException("Input file with weather table can't be null", "file");
        if(this.temperatureInAreas == null) this.temperatureInAreas = new HashMap<String, String[][]>();
        if(this.year == null || this.year.isEmpty()) throw new NullObjectException("Year can't be null or empty, it is used like name of outputXml file", "year");
        if(!htmlFile.exists()) throw new InternalErrorException("Html file on path: " + htmlFile.getAbsolutePath() + " not exists.");
        
        if(useOldParser) this.oldParser();
        else this.xqueryParser();
    }
    
    /**
     * Private method for parsing htmlFile by xquery to xmlFile.
     * If all is ok, save data to ?.xml file in xmls/?.xml path in actual working directory.
     * ?.xml = year.xml, ex. 1999.xml
     * 
     * --XQUERY_FILE_NAME is const here, looking for name in actual working directory
     */
    private void xqueryParser() throws InternalErrorException, IOException, XPathException {
        Configuration config = new Configuration();
        StaticQueryContext staticContext = config.newStaticQueryContext();
        XQueryExpression exp = staticContext.compileQuery(new FileReader(XQUERY_FILE_NAME));
        
        DynamicQueryContext dynamicContext = new DynamicQueryContext(config);
        
        Properties props = new Properties();
        props.setProperty(OutputKeys.METHOD, "xml");
        props.setProperty(OutputKeys.INDENT, "yes");
              
        File file = new File("xmls/" + year + ".xml");
        FileWriter fw = new FileWriter(file.getAbsoluteFile());
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
        exp.run(dynamicContext, new StreamResult(bw), props);
        fw.close();
        bw.close();
        
        this.setXmlFile(file);
    }
   
    /**
     * Convert xmlFile to String (if file aready exist, was created)
     * 
     * @return xmlFile in String
     * 
     * @throws InternalErrorException when some internal error occurs
     * @throws IOException input output exception when works with files
     */
    public String xmlToString() throws InternalErrorException, IOException {
        if(xmlFile == null) throw new NullObjectException("xmlFile is not initialized", "xmlFile");
        if(!xmlFile.exists()) throw new InternalErrorException("Xml file not exists on path " + xmlFile.toString());
        
        FileInputStream fstream = new FileInputStream(xmlFile);
        DataInputStream in = new DataInputStream(fstream);
        BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
        
        StringBuilder sb = new StringBuilder();
        String strLine;
        while ((strLine = br.readLine()) != null) {
            sb.append(strLine);
            sb.append('\n');
        }
        fstream.close();
        in.close();
        br.close();
        
        return sb.toString();
    }
    
    // -----  ----- IMPORTANT !!! ----- -----
    //All below methods are old and deprycated (these are methods about classical text parser, now new parser is xquery parser)
    
    /**
     * Old method of parsing htmlFile.
     * -- row by row parsing file and information save to map temperatureInAreas
     * 
     * @return structure map of String to String[][]
     * 
     * @throws InternalErrorException when some internal error occurs
     * @throws IOException input output exception when works with files
     * @deprecated cause this version is not supported now
     */
    @Deprecated
    private Map<String, String[][]> oldParser() throws InternalErrorException, IOException {
        FileInputStream fstream = new FileInputStream(htmlFile);
        DataInputStream in = new DataInputStream(fstream);
        BufferedReader br = new BufferedReader(new InputStreamReader(in, outputCharset));
        String strLine;

        int rowNumber = 0;
        String matchOnArea = null;
        String[][] row = new String[3][13];
        while ((strLine = br.readLine()) != null) {
            Matcher testOfLine = temperature.matcher(strLine);
            if(testOfLine.find()) {
                Matcher area = areas.matcher(strLine);
                
                if(area.find()) {
                    matchOnArea = area.group();
                    matchOnArea = matchOnArea.substring(2, matchOnArea.length()-1);
                } 

                Matcher temp = temperature.matcher(strLine);
                int columntNumber = 0;

                while(temp.find()){
                    String matchOnNumber = temp.group();
                    matchOnNumber = matchOnNumber.substring(1, matchOnNumber.length()-1);
                    matchOnNumber = matchOnNumber.replace(",", ".");
                    if(matchOnNumber.equals("umber\">&nbsp;")) row[rowNumber][columntNumber] = null;
                    else row[rowNumber][columntNumber] = matchOnNumber;
                    columntNumber++;
                    if(columntNumber>12 && rowNumber==2) {
                        temperatureInAreas.put(matchOnArea, Utils.copyTwoDimensionalArrayOfStrings(row));
                    }
                }
                rowNumber++;
                if(rowNumber>2) rowNumber=0;
            }
        }
        in.close();
        fstream.close();
        br.close();
        return temperatureInAreas;
    }
    
    /**
     * NOT USED WITH NEW PARSER
     * This method get String from map temperatureInAreas
     * 
     * @return temperatureInAreas in String
     * 
     * @throws InternalErrorException when some internal error occurs
     * @deprecated caused this method can be used only together with old parser
     */
    @Deprecated
    public String temperatureInAreasToString() throws InternalErrorException {
        if(this.temperatureInAreas == null) throw new NullObjectException("Map of temp");
        if(this.temperatureInAreas.keySet() == null || this.temperatureInAreas.keySet().isEmpty()) return new String("No keys or values");
        
        StringBuffer sb = new StringBuffer();
        
        Set<String> keys = temperatureInAreas.keySet();
        for(String key: keys) {
            String[][] a = temperatureInAreas.get(key);
            sb.append("For key : '");
            sb.append(key + "'" + '\n');
            sb.append("Values for T = ");
            for(int i=0;i<a[0].length;i++) {
              if(i!= a[0].length-1) {
                 sb.append(a[0][i] + ", ");
              } else {
                sb.append(a[0][i]); 
              }
            }
            sb.append('\n');
            sb.append("Values for N = ");
            for(int i=0;i<a[1].length;i++) {
              if(i!= a[1].length-1) {
                sb.append(a[1][i] + ", ");  
              } else {
                sb.append(a[1][i]);  
              }    
            }
            sb.append('\n');
            sb.append("Values for O = ");
            for(int i=0;i<a[2].length;i++) {
              if(i!= a[2].length-1) {
                sb.append(a[2][i] + ", ");
              } else {
                sb.append(a[2][i]);  
              }    
            }
            sb.append('\n');
        }
        
        return sb.toString();
    }
    
    /**
     * NOT USE WITH NEW PARSER
     * Parse htmlFile to xml file specific by xmlFileName.
     * Use row parsing of file.
     * 
     * @param xmlFileName specific outputXml file name
     * @param xmlCharset specific outputXml charSet
     * @param year specific year of xml data structure (year of table with weather)
     *
     * @return File of created xml file
     * 
     * @throws InternalErrorException when some internal error occurs
     * @throws IOException input output exception when works with files
     * @deprecated this method is used only with old parser 
     */
    @Deprecated
    public File convertDataToXML(String xmlFileName, String xmlCharset, String year) throws IOException, InternalErrorException {
        if(xmlFileName == null) throw new NullObjectException("File to parse xml can't be null.", "xmlFile");
        if(year == null) throw new NullObjectException("Year must be set.", "year");
        if(xmlCharset == null) xmlCharset = "UTF-8";
        
        xmlFileName = "xmls/" + xmlFileName;
        File xmlFile = new File(xmlFileName);
        if(!xmlFile.exists()) xmlFile.createNewFile();
        
        return this.createXml(xmlFile, xmlCharset, year);
    }
    
    /**
     * Private method for creating XML file from temperatureInAreas..
     * @deprecated use only with old parser
     */
    @Deprecated
    private File createXml(File xmlFile, String xmlCharset, String year) throws IOException, InternalErrorException {
        FileWriter fw = new FileWriter(xmlFile.getAbsoluteFile());
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(xmlFile), xmlCharset));
        
        bw.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        bw.append('\n');
        bw.append("<weather>");
        bw.append('\n');
        bw.append("<dates year='" + year + "'>");
        bw.append('\n');
        
        if(this.temperatureInAreas != null && !this.temperatureInAreas.keySet().isEmpty()) {
            Set<String> keySet = this.temperatureInAreas.keySet();
            for(String key: keySet) {
                bw.append("<area region='" + key + "'>");
                bw.append('\n');

                String[][] temperatures = this.temperatureInAreas.get(key);
                if(temperatures.length != 3) throw new InternalErrorException("The array with values has not normal size.");
                for(int i=0;i<temperatures[0].length;i++) {
                    if(i!=12) bw.append("<date month='" + (i+1) + "'>");
                    else bw.append("<date>");
                    bw.append('\n');
                    bw.append("<temperature type='air'>" + (temperatures[0][i] == null ? "n/a" : temperatures[0][i]) + "</temperature>");
                    bw.append('\n');
                    bw.append("<temperature type='normal'>" + (temperatures[1][i] == null ? "n/a" : temperatures[1][i]) + "</temperature>");
                    bw.append('\n');
                    bw.append("<temperature type='divergence'>" + (temperatures[2][i] == null ? "n/a" : temperatures[2][i]) + "</temperature>");
                    bw.append('\n');
                    bw.append("</date>");
                    bw.append('\n');
                }

                bw.append("</area>");
                bw.append('\n');
            }
        }
        bw.append("</dates>");
        bw.append('\n');
        bw.append("</weather>");
        
        bw.close();
        return xmlFile;
    }
    
    /**
     * Get xml output as String. Use temperatureInAreas
     * 
     * @return xml file as String
     * @throws InternalErrorException when some internal error occurs
     * @deprecated use only with old Parser
     */
    @Deprecated
    public String getXmlAsString() throws InternalErrorException {
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        sb.append('\n');
        sb.append("<weather>");
        sb.append('\n');
        sb.append("<dates year='" + year + "'>");
        sb.append('\n');
        if(this.temperatureInAreas != null && !this.temperatureInAreas.keySet().isEmpty()) {
            Set<String> keySet = this.temperatureInAreas.keySet();
            for(String key: keySet) {
                sb.append("<area region='" + key + "'>");
                sb.append('\n');

                String[][] temperatures = this.temperatureInAreas.get(key);
                if(temperatures.length != 3) throw new InternalErrorException("The array with values has not normal size.");
                for(int i=0;i<temperatures[0].length;i++) {
                    if(i!=12) sb.append("<date month='" + (i+1) + "'>");
                    else sb.append("<date>");
                    sb.append('\n');
                    sb.append("<temperature type='air'>" + (temperatures[0][i] == null ? "n/a" : temperatures[0][i]) + "</temperature>");
                    sb.append('\n');
                    sb.append("<temperature type='normal'>" + (temperatures[1][i] == null ? "n/a" : temperatures[1][i]) + "</temperature>");
                    sb.append('\n');
                    sb.append("<temperature type='divergence'>" + (temperatures[2][i] == null ? "n/a" : temperatures[2][i]) + "</temperature>");
                    sb.append('\n');
                    sb.append("</date>");
                    sb.append('\n');
                }

                sb.append("</area>");
                sb.append('\n');
            }
        }
        sb.append("</dates>");
        sb.append('\n');
        sb.append("</weather>");
        return sb.toString();
    }
}